#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>

#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>

#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/serialize.h>

#include <cctype>
#include <optional>
#include <tuple>

namespace ripple {

class Transaction_test : public beast::unit_test::suite
{
    std::unique_ptr<Config>
    makeNetworkConfig(uint32_t networkID)
    {
        using namespace test::jtx;
        return envconfig([&](std::unique_ptr<Config> cfg) {
            cfg->NETWORK_ID = networkID;
            return cfg;
        });
    }

    void
    testRangeRequest(FeatureBitset features)
    {
        testcase("Test Range Request");

        using namespace test::jtx;
        using std::to_string;

        char const* COMMAND = jss::tx.c_str();
        char const* BINARY = jss::binary.c_str();
        char const* NOT_FOUND = RPC::get_error_info(rpcTXN_NOT_FOUND).token;
        char const* INVALID = RPC::get_error_info(rpcINVALID_LGR_RANGE).token;
        char const* EXCESSIVE =
            RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;

        Env env{*this, features};
        auto const alice = Account("alice");
        env.fund(XRP(1000), alice);
        env.close();

        std::vector<std::shared_ptr<STTx const>> txns;
        std::vector<std::shared_ptr<STObject const>> metas;
        auto const startLegSeq = env.current()->info().seq;
        for (int i = 0; i < 750; ++i)
        {
            env(noop(alice));
            txns.emplace_back(env.tx());
            env.close();
            metas.emplace_back(
                env.closed()->txRead(env.tx()->getTransactionID()).second);
        }
        auto const endLegSeq = env.closed()->info().seq;

        // Find the existing transactions
        for (size_t i = 0; i < txns.size(); ++i)
        {
            auto const& tx = txns[i];
            auto const& meta = metas[i];
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(
                result[jss::result][jss::tx] ==
                strHex(tx->getSerializer().getData()));
            BEAST_EXPECT(
                result[jss::result][jss::meta] ==
                strHex(meta->getSerializer().getData()));
        }

        auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx;
        for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq + deltaEndSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            if (deltaEndSeq)
                BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
            else
                BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
        }

        // Find transactions outside of provided range.
        for (auto&& tx : txns)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(endLegSeq + 1),
                to_string(endLegSeq + 100));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        auto const deletedLedger = (startLegSeq + endLegSeq) / 2;
        {
            // Remove one of the ledgers from the database directly
            dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())
                ->deleteTransactionByLedgerSeq(deletedLedger);
        }

        for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq + deltaEndSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);
            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                to_string(startLegSeq),
                to_string(endLegSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                to_string(startLegSeq),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(txns[0]->getTransactionID()),
                to_string(startLegSeq),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min > max)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(deletedLedger - 1),
                to_string(startLegSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min < 0)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(-1),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min < 0, max < 0)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(-20),
                to_string(-10));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (only one value)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(20));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (only one value)
        {
            auto const result = env.rpc(
                COMMAND, to_string(tx->getTransactionID()), to_string(20));

            // Since we only provided one value for the range,
            // the interface parses it as a false binary flag,
            // as single-value ranges are not accepted. Since
            // the error this causes differs depending on the platform
            // we don't call out a specific error here.
            BEAST_EXPECT(result[jss::result][jss::status] == jss::error);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (max - min > 1000)
        {
            auto const result = env.rpc(
                COMMAND,
                to_string(tx->getTransactionID()),
                BINARY,
                to_string(startLegSeq),
                to_string(startLegSeq + 1001));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == EXCESSIVE);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }
    }

    void
    testRangeCTIDRequest(FeatureBitset features)
    {
        testcase("CTID Range Request");

        using namespace test::jtx;
        using std::to_string;

        char const* COMMAND = jss::tx.c_str();
        char const* BINARY = jss::binary.c_str();
        char const* NOT_FOUND = RPC::get_error_info(rpcTXN_NOT_FOUND).token;
        char const* INVALID = RPC::get_error_info(rpcINVALID_LGR_RANGE).token;
        char const* EXCESSIVE =
            RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;

        Env env{*this, makeNetworkConfig(11111)};
        uint32_t netID = env.app().config().NETWORK_ID;

        auto const alice = Account("alice");
        env.fund(XRP(1000), alice);
        env.close();

        std::vector<std::shared_ptr<STTx const>> txns;
        std::vector<std::shared_ptr<STObject const>> metas;
        auto const startLegSeq = env.current()->info().seq;
        for (int i = 0; i < 750; ++i)
        {
            env(noop(alice));
            txns.emplace_back(env.tx());
            env.close();
            metas.emplace_back(
                env.closed()->txRead(env.tx()->getTransactionID()).second);
        }
        auto const endLegSeq = env.closed()->info().seq;

        // Find the existing transactions
        for (size_t i = 0; i < txns.size(); ++i)
        {
            auto const& tx = txns[i];
            auto const& meta = metas[i];
            uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
            auto const result = env.rpc(
                COMMAND,
                *RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(
                result[jss::result][jss::tx] ==
                strHex(tx->getSerializer().getData()));
            BEAST_EXPECT(
                result[jss::result][jss::meta] ==
                strHex(meta->getSerializer().getData()));
        }

        auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx;
        auto const ctid = *RPC::encodeCTID(endLegSeq, tx->getSeqValue(), netID);
        for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq + deltaEndSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            if (deltaEndSeq)
                BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
            else
                BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Find transactions outside of provided range.
        for (size_t i = 0; i < txns.size(); ++i)
        {
            // auto const& tx = txns[i];
            auto const& meta = metas[i];
            uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
            auto const result = env.rpc(
                COMMAND,
                *RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
                BINARY,
                to_string(endLegSeq + 1),
                to_string(endLegSeq + 100));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        auto const deletedLedger = (startLegSeq + endLegSeq) / 2;
        {
            // Remove one of the ledgers from the database directly
            dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())
                ->deleteTransactionByLedgerSeq(deletedLedger);
        }

        for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                BINARY,
                to_string(startLegSeq),
                to_string(endLegSeq + deltaEndSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);
            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const result = env.rpc(
                COMMAND, ctid, to_string(startLegSeq), to_string(endLegSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                to_string(startLegSeq),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == NOT_FOUND);

            BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
        }

        // Provide range without providing the `binary`
        // field. (Tests parameter parsing)
        {
            auto const& meta = metas[0];
            uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
            auto const result = env.rpc(
                COMMAND,
                *RPC::encodeCTID(endLegSeq, txnIdx, netID),
                to_string(startLegSeq),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min > max)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                BINARY,
                to_string(deletedLedger - 1),
                to_string(startLegSeq));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min < 0)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                BINARY,
                to_string(-1),
                to_string(deletedLedger - 1));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (min < 0, max < 0)
        {
            auto const result =
                env.rpc(COMMAND, ctid, BINARY, to_string(-20), to_string(-10));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (only one value)
        {
            auto const result = env.rpc(COMMAND, ctid, BINARY, to_string(20));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == INVALID);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (only one value)
        {
            auto const result = env.rpc(COMMAND, ctid, to_string(20));

            // Since we only provided one value for the range,
            // the interface parses it as a false binary flag,
            // as single-value ranges are not accepted. Since
            // the error this causes differs depending on the platform
            // we don't call out a specific error here.
            BEAST_EXPECT(result[jss::result][jss::status] == jss::error);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }

        // Provide an invalid range: (max - min > 1000)
        {
            auto const result = env.rpc(
                COMMAND,
                ctid,
                BINARY,
                to_string(startLegSeq),
                to_string(startLegSeq + 1001));

            BEAST_EXPECT(
                result[jss::result][jss::status] == jss::error &&
                result[jss::result][jss::error] == EXCESSIVE);

            BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
        }
    }

    void
    testCTIDValidation(FeatureBitset features)
    {
        testcase("CTID Validation");

        using namespace test::jtx;
        using std::to_string;

        Env env{*this, makeNetworkConfig(11111)};

        // Test case 1: Valid input values
        auto const expected11 = std::optional<std::string>("CFFFFFFFFFFFFFFF");
        BEAST_EXPECT(
            RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11);
        auto const expected12 = std::optional<std::string>("C000000000000000");
        BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12);
        auto const expected13 = std::optional<std::string>("C000000100020003");
        BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13);
        auto const expected14 = std::optional<std::string>("C0CA2AA7326FFFFF");
        BEAST_EXPECT(RPC::encodeCTID(13249191UL, 12911U, 65535U) == expected14);

        // Test case 2: ledger_seq greater than 0xFFFFFFF
        BEAST_EXPECT(!RPC::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU));

        // Test case 3: txn_index greater than 0xFFFF
        BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF));

        // Test case 4: network_id greater than 0xFFFF
        BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U));

        // Test case 5: Valid input values
        auto const expected51 =
            std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                std::make_tuple(0, 0, 0));
        BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51);
        auto const expected52 =
            std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                std::make_tuple(1U, 2U, 3U));
        BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52);
        auto const expected53 =
            std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                std::make_tuple(13249191UL, 12911U, 49221U));
        BEAST_EXPECT(RPC::decodeCTID("C0CA2AA7326FC045") == expected53);

        // Test case 6: ctid not a string or big int
        BEAST_EXPECT(!RPC::decodeCTID(0xCFF));

        // Test case 7: ctid not a hexadecimal string
        BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFFG"));

        // Test case 8: ctid not exactly 16 nibbles
        BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFF"));

        // Test case 9: ctid too large to be a valid CTID value
        BEAST_EXPECT(!RPC::decodeCTID("CFFFFFFFFFFFFFFFF"));

        // Test case 10: ctid doesn't start with a C nibble
        BEAST_EXPECT(!RPC::decodeCTID("FFFFFFFFFFFFFFFF"));

        // Test case 11: Valid input values
        BEAST_EXPECT(
            (RPC::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) ==
             std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                 std::make_tuple(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU))));
        BEAST_EXPECT(
            (RPC::decodeCTID(0xC000'0000'0000'0000ULL) ==
             std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                 std::make_tuple(0, 0, 0))));
        BEAST_EXPECT(
            (RPC::decodeCTID(0xC000'0001'0002'0003ULL) ==
             std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                 std::make_tuple(1U, 2U, 3U))));
        BEAST_EXPECT(
            (RPC::decodeCTID(0xC0CA'2AA7'326F'C045ULL) ==
             std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
                 std::make_tuple(1324'9191UL, 12911U, 49221U))));

        // Test case 12: ctid not exactly 16 nibbles
        BEAST_EXPECT(!RPC::decodeCTID(0xC003'FFFF'FFFF'FFF));

        // Test case 13: ctid too large to be a valid CTID value
        // this test case is not possible in c++ because it would overflow the
        // type, left in for completeness
        // BEAST_EXPECT(!RPC::decodeCTID(0xCFFFFFFFFFFFFFFFFULL));

        // Test case 14: ctid doesn't start with a C nibble
        BEAST_EXPECT(!RPC::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL));
    }

    void
    testCTIDRPC(FeatureBitset features)
    {
        testcase("CTID RPC");

        using namespace test::jtx;

        // Use a Concise Transaction Identifier to request a transaction.
        for (uint32_t netID : {11111, 65535, 65536})
        {
            Env env{*this, makeNetworkConfig(netID)};
            BEAST_EXPECT(netID == env.app().config().NETWORK_ID);

            auto const alice = Account("alice");
            auto const bob = Account("bob");

            auto const startLegSeq = env.current()->info().seq;
            env.fund(XRP(10000), alice, bob);
            env(pay(alice, bob, XRP(10)));
            env.close();

            auto const ctid = RPC::encodeCTID(startLegSeq, 0, netID);
            if (netID > 0xFFFF)
            {
                // Concise transaction IDs do not support a network ID > 0xFFFF.
                BEAST_EXPECT(ctid == std::nullopt);
                continue;
            }

            Json::Value jsonTx;
            jsonTx[jss::binary] = false;
            jsonTx[jss::ctid] = *ctid;
            jsonTx[jss::id] = 1;
            auto const jrr =
                env.rpc("json", "tx", to_string(jsonTx))[jss::result];
            BEAST_EXPECT(jrr[jss::ctid] == ctid);
            BEAST_EXPECT(jrr.isMember(jss::hash));
        }

        // test querying with mixed case ctid
        {
            Env env{*this, makeNetworkConfig(11111)};
            std::uint32_t const netID = env.app().config().NETWORK_ID;

            Account const alice = Account("alice");
            Account const bob = Account("bob");

            std::uint32_t const startLegSeq = env.current()->info().seq;
            env.fund(XRP(10000), alice, bob);
            env(pay(alice, bob, XRP(10)));
            env.close();

            std::string const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
            auto isUpper = [](char c) { return std::isupper(c) != 0; };

            // Verify that there are at least two upper case letters in ctid and
            // test a mixed case
            if (BEAST_EXPECT(
                    std::count_if(ctid.begin(), ctid.end(), isUpper) > 1))
            {
                // Change the first upper case letter to lower case.
                std::string mixedCase = ctid;
                {
                    auto const iter = std::find_if(
                        mixedCase.begin(), mixedCase.end(), isUpper);
                    *iter = std::tolower(*iter);
                }
                BEAST_EXPECT(ctid != mixedCase);

                Json::Value jsonTx;
                jsonTx[jss::binary] = false;
                jsonTx[jss::ctid] = mixedCase;
                jsonTx[jss::id] = 1;
                Json::Value const jrr =
                    env.rpc("json", "tx", to_string(jsonTx))[jss::result];
                BEAST_EXPECT(jrr[jss::ctid] == ctid);
                BEAST_EXPECT(jrr[jss::hash]);
            }
        }

        // test that if the network is 65535 the ctid is not in the response
        // Using a hash to request the transaction, test the network ID
        // boundary where the CTID is (not) in the response.
        for (uint32_t netID : {2, 1024, 65535, 65536})
        {
            Env env{*this, makeNetworkConfig(netID)};
            BEAST_EXPECT(netID == env.app().config().NETWORK_ID);

            auto const alice = Account("alice");
            auto const bob = Account("bob");

            env.fund(XRP(10000), alice, bob);
            env(pay(alice, bob, XRP(10)));
            env.close();

            auto const ledgerSeq = env.current()->info().seq;

            env(noop(alice), ter(tesSUCCESS));
            env.close();

            Json::Value params;
            params[jss::id] = 1;
            auto const hash = env.tx()->getJson(JsonOptions::none)[jss::hash];
            params[jss::transaction] = hash;
            auto const jrr =
                env.rpc("json", "tx", to_string(params))[jss::result];
            BEAST_EXPECT(jrr[jss::hash] == hash);

            BEAST_EXPECT(jrr.isMember(jss::ctid) == (netID <= 0xFFFF));
            if (jrr.isMember(jss::ctid))
            {
                auto const ctid = RPC::encodeCTID(ledgerSeq, 0, netID);
                BEAST_EXPECT(jrr[jss::ctid] == *ctid);
            }
        }

        // test the wrong network ID was submitted
        {
            Env env{*this, makeNetworkConfig(21337)};
            uint32_t netID = env.app().config().NETWORK_ID;

            auto const alice = Account("alice");
            auto const bob = Account("bob");

            auto const startLegSeq = env.current()->info().seq;
            env.fund(XRP(10000), alice, bob);
            env(pay(alice, bob, XRP(10)));
            env.close();

            auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID + 1);
            Json::Value jsonTx;
            jsonTx[jss::binary] = false;
            jsonTx[jss::ctid] = ctid;
            jsonTx[jss::id] = 1;
            auto const jrr =
                env.rpc("json", "tx", to_string(jsonTx))[jss::result];
            BEAST_EXPECT(jrr[jss::error] == "wrongNetwork");
            BEAST_EXPECT(jrr[jss::error_code] == rpcWRONG_NETWORK);
            BEAST_EXPECT(
                jrr[jss::error_message] ==
                "Wrong network. You should submit this request to a node "
                "running on NetworkID: 21338");
        }
    }

    void
    testRequest(FeatureBitset features, unsigned apiVersion)
    {
        testcase("Test Request API version " + std::to_string(apiVersion));

        using namespace test::jtx;
        using std::to_string;

        Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
                    cfg->FEES.reference_fee = 10;
                    return cfg;
                })};
        Account const alice{"alice"};
        Account const alie{"alie"};
        Account const gw{"gw"};
        auto const USD{gw["USD"]};

        env.fund(XRP(1000000), alice, gw);
        env.close();

        // AccountSet
        env(noop(alice));

        // Payment
        env(pay(alice, gw, XRP(100)));

        std::shared_ptr<STTx const> txn = env.tx();
        env.close();
        std::shared_ptr<STObject const> meta =
            env.closed()->txRead(env.tx()->getTransactionID()).second;

        Json::Value expected = txn->getJson(JsonOptions::none);
        expected[jss::DeliverMax] = expected[jss::Amount];
        if (apiVersion > 1)
        {
            expected.removeMember(jss::hash);
            expected.removeMember(jss::Amount);
        }

        Json::Value const result = {[&env, txn, apiVersion]() {
            Json::Value params{Json::objectValue};
            params[jss::transaction] = to_string(txn->getTransactionID());
            params[jss::binary] = false;
            params[jss::api_version] = apiVersion;
            return env.client().invoke("tx", params);
        }()};

        BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
        if (apiVersion > 1)
        {
            BEAST_EXPECT(
                result[jss::result][jss::close_time_iso] ==
                "2000-01-01T00:00:20Z");
            BEAST_EXPECT(
                result[jss::result][jss::hash] ==
                to_string(txn->getTransactionID()));
            BEAST_EXPECT(result[jss::result][jss::validated] == true);
            BEAST_EXPECT(result[jss::result][jss::ledger_index] == 4);
            BEAST_EXPECT(
                result[jss::result][jss::ledger_hash] ==
                "B41882E20F0EC6228417D28B9AE0F33833645D35F6799DFB782AC97FC4BB51"
                "D2");
        }

        for (auto memberIt = expected.begin(); memberIt != expected.end();
             memberIt++)
        {
            std::string const name = memberIt.memberName();
            auto const& result_transaction =
                (apiVersion > 1 ? result[jss::result][jss::tx_json]
                                : result[jss::result]);
            if (BEAST_EXPECT(result_transaction.isMember(name)))
            {
                auto const received = result_transaction[name];
                BEAST_EXPECTS(
                    received == *memberIt,
                    "Transaction contains \n\"" + name + "\": "  //
                        + to_string(received)                    //
                        + " but expected "                       //
                        + to_string(expected));
            }
        }
    }

    void
    testBinaryRequest(unsigned apiVersion)
    {
        testcase(
            "Test binary request API version " + std::to_string(apiVersion));

        using namespace test::jtx;
        using std::to_string;

        Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
                    cfg->FEES.reference_fee = 10;
                    return cfg;
                })};
        Account const alice{"alice"};
        Account const gw{"gw"};
        auto const USD{gw["USD"]};

        env.fund(XRP(1000000), alice, gw);
        std::shared_ptr<STTx const> const txn = env.tx();
        BEAST_EXPECT(
            to_string(txn->getTransactionID()) ==
            "3F8BDE5A5F82C4F4708E5E9255B713E303E6E1A371FD5C7A704AFD1387C23981");
        env.close();
        std::shared_ptr<STObject const> meta =
            env.closed()->txRead(txn->getTransactionID()).second;

        std::string const expected_tx_blob = serializeHex(*txn);
        std::string const expected_meta_blob = serializeHex(*meta);

        Json::Value const result = [&env, txn, apiVersion]() {
            Json::Value params{Json::objectValue};
            params[jss::transaction] = to_string(txn->getTransactionID());
            params[jss::binary] = true;
            params[jss::api_version] = apiVersion;
            return env.client().invoke("tx", params);
        }();

        if (BEAST_EXPECT(result[jss::status] == "success"))
        {
            BEAST_EXPECT(result[jss::result][jss::status] == "success");
            BEAST_EXPECT(result[jss::result][jss::validated] == true);
            BEAST_EXPECT(
                result[jss::result][jss::hash] ==
                to_string(txn->getTransactionID()));
            BEAST_EXPECT(result[jss::result][jss::ledger_index] == 3);
            BEAST_EXPECT(result[jss::result][jss::ctid] == "C000000300030000");

            if (apiVersion > 1)
            {
                BEAST_EXPECT(
                    result[jss::result][jss::tx_blob] == expected_tx_blob);
                BEAST_EXPECT(
                    result[jss::result][jss::meta_blob] == expected_meta_blob);
                BEAST_EXPECT(
                    result[jss::result][jss::ledger_hash] ==
                    "2D5150E5A5AA436736A732291E437ABF01BC9E206C2DF3C77C4F856915"
                    "7905AA");
                BEAST_EXPECT(
                    result[jss::result][jss::close_time_iso] ==
                    "2000-01-01T00:00:10Z");
            }
            else
            {
                BEAST_EXPECT(result[jss::result][jss::tx] == expected_tx_blob);
                BEAST_EXPECT(
                    result[jss::result][jss::meta] == expected_meta_blob);
                BEAST_EXPECT(result[jss::result][jss::date] == 10);
            }
        }
    }

public:
    void
    run() override
    {
        using namespace test::jtx;
        forAllApiVersions(
            std::bind_front(&Transaction_test::testBinaryRequest, this));

        FeatureBitset const all{testable_amendments()};
        testWithFeats(all);
    }

    void
    testWithFeats(FeatureBitset features)
    {
        testRangeRequest(features);
        testRangeCTIDRequest(features);
        testCTIDValidation(features);
        testCTIDRPC(features);
        forAllApiVersions(
            std::bind_front(&Transaction_test::testRequest, this, features));
    }
};

BEAST_DEFINE_TESTSUITE(Transaction, rpc, ripple);

}  // namespace ripple
